﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Nes4Net
{
    public class Nes
    {
        public Cpu Cpu { get; internal set; }
        public Apu Apu { get; internal set; }
        public Ppu Ppu { get; internal set; }
        public Mapper0 Mmap { get; internal set; }
        public NesOpts Opts { get; internal set; }
        public Controller[] Controllers { get; internal set; }

        public NesUI UI { get; private set; }
        public Rom Rom { get; internal set; }
        public Int32 FrameTime { get; internal set; }



        public Nes(NesOpts opts)
        {
            this.Opts = NesOpts.Default;
            this.Opts.OnFrame = opts.OnFrame;
            this.Opts.OnAudioSample = opts.OnAudioSample;
            this.Opts.PreferredFrameRate = opts.PreferredFrameRate;
            this.Opts.EmulateSound = opts.EmulateSound;
            this.Opts.OnStatusUpdate = opts.OnStatusUpdate;


            //this.Opts = opts;
            this.FrameTime = 1000 / this.Opts.PreferredFrameRate;
            this.UI = new NesUI {
                WriteFrame = this.Opts.OnFrame,
                UpdateStatus = this.Opts.OnStatusUpdate,
              };
            this.Cpu = new Cpu(this);
            this.Ppu = new Ppu(this);
            this.Apu = new Apu(this);
            this.Mmap = null; // set in loadROM()
            this.Controllers = new Controller[] {
                new Controller(),
                new Controller(),
            };

            this.UI.UpdateStatus("Ready to load a ROM.");

            //this.Frame = this.Frame.bind(this);
            //this.ButtonDown = this.ButtonDown.bind(this);
            //this.ButtonUp = this.ButtonUp.bind(this);
            //this.zapperMove = this.zapperMove.bind(this);
            //this.zapperFireDown = this.zapperFireDown.bind(this);
            //this.zapperFireUp = this.zapperFireUp.bind(this);
        }

        #region Property
        public Int32 FpsFrameCount = 0;
        public Byte[] RomData = null;
        public DateTime LastFpsTime;
        internal string crashMessage;

        private void Reset ()
        {
            if (this.Mmap != null)
            {
                this.Mmap.Reset();
            }

            this.Cpu.Reset();
            this.Ppu.reset();
            this.Apu.Reset();

            //this.LastFpsTime = null;
            this.FpsFrameCount = 0;
        }

        public void Frame()
        {
            this.Ppu.startFrame();
            var emulateSound = this.Opts.EmulateSound;
            var cpu = this.Cpu;
            var ppu = this.Ppu;
            var papu = this.Apu;
            int cycles;
            while (true)
            {
                if (cpu.CyclesToHalt == 0)
                {
                    // Execute a CPU instruction
                    cycles = cpu.Emulate();
                    if (emulateSound)
                    {
                        papu.clockFrameCounter(cycles);
                    }
                    cycles *= 3;
                }
                else if (cpu.CyclesToHalt > 8)
                {
                    cycles = 24;
                    if (emulateSound)
                    {
                        papu.clockFrameCounter(8);
                    }
                    cpu.CyclesToHalt -= 8;
                }
                else
                {
                    cycles = cpu.CyclesToHalt * 3;
                    if (emulateSound)
                    {
                        papu.clockFrameCounter(cpu.CyclesToHalt);
                    }
                    cpu.CyclesToHalt = 0;
                }

                for (; cycles > 0; cycles--)
                {
                    if (ppu.curX == ppu.spr0HitX 
                        && ppu.f_spVisibility == 1 
                        && ppu.scanline - 21 == ppu.spr0HitY)
                    {
                        // Set sprite 0 hit flag:
                        ppu.setStatusFlag(Ppu.STATUS_SPRITE0HIT, true);
                    }

                    if (ppu.requestEndFrame)
                    {
                        ppu.nmiCounter--;
                        if (ppu.nmiCounter == 0)
                        {
                            ppu.requestEndFrame = false;
                            ppu.startVBlank();
                            goto FRAMELOOP;
                        }
                    }

                    ppu.curX++;
                    if (ppu.curX == 341)
                    {
                        ppu.curX = 0;
                        ppu.endScanline();
                    }
                }
            }
        FRAMELOOP:
            this.FpsFrameCount++;
        }

        public void ButtonDown (Int32 controller, Int32 button)
        {
            this.Controllers[controller].ButtonDown(button);
        }

        public void ButtonUp (Int32 controller, Int32 button)
        {
            this.Controllers[controller].ButtonUp(button);
        }

        public void ApperMove(Int32 x, Int32 y)
        {
            if (this.Mmap is null) return;
            this.Mmap.zapperX = x;
            this.Mmap.zapperY = y;
        }

        public void ZapperFireDown ()
        {
            if (this.Mmap is null) return;
            this.Mmap.zapperFired = true;
        }

        public void ZapperFireUp ()
        {
            if (this.Mmap is null) return;
            this.Mmap.zapperFired = false;
        }

        public Int32 GetFPS()
        {
            var now = DateTime.Now;
            Int32 fps;
                fps = (Int32)(this.FpsFrameCount / ((now - this.LastFpsTime).TotalMilliseconds / 1000));
            //if (this.LastFpsTime)
            //{
            //}
            this.FpsFrameCount = 0;
            this.LastFpsTime = now;
            return fps;
        }

        public void ReloadROM()
        {
            if (this.RomData != null)
            {
                this.LoadROM(this.RomData);
            }
        }

        // Loads a ROM file into the CPU and PPU.
        // The ROM file is validated first.
        public void LoadROM(Byte[] data)
        {
            // Load ROM file:
            this.Rom = new Rom(this);
            this.Rom.Load(data);

            this.Reset();
            this.Mmap = this.Rom.createMapper();
            this.Mmap.LoadROM();
            this.Ppu.setMirroring(this.Rom.GetMirroringType());
            this.RomData = data;
        }

        public void SetFramerate(Int32 rate)
        {
            this.Opts.PreferredFrameRate = rate;
            this.FrameTime = 1000 / rate;
            // 未知代码
            //this.Papu.SetSampleRate(this.Opts.SampleRate, false);
        }
        #endregion
    }

    public class NesOpts
    {
        public Action<Int32[]> OnFrame;
        public Action<Int32, Int32> OnAudioSample;
        public Action<String> OnStatusUpdate;
        public Action<Int32, Int32> OnBatteryRamWrite;
        public Int32 PreferredFrameRate = 60;
        public Boolean EmulateSound = false;
        public Int32 SampleRate = 48000;

        public static NesOpts Default = new NesOpts
        {
            OnFrame = buff => { },
            OnAudioSample = (l, f) => { },
            OnStatusUpdate = str => { },
            OnBatteryRamWrite = (l, f) => { },
            PreferredFrameRate = 60,
            EmulateSound = true,
            SampleRate = 44100
        };
    }

    public class NesUI
    {
        public Action<Int32[]> WriteFrame;
        public Action<String> UpdateStatus;
    }
}
